package com.infra.micro.id.domain.core.snowflake;

import com.infra.common.exception.BizException;
import com.infra.micro.id.domain.core.AbstractIdGenerator;
import com.infra.micro.id.domain.repository.IdWorkerRepository;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.Set;

/**
 * 雪花算法id生成器
 *
 * @author PD
 **/
@Component
public class IdSnowflakeGenerator extends AbstractIdGenerator {
    /**
     * 起始的时间戳
     */
    protected long epoch;
    /**
     * 时间戳占用的位数
     */
    protected final long timestampBits = 41L;
    /**
     * 序列号占用的位数
     */
    protected final long sequenceBits = 12L;
    /**
     * 机器标识占用的位置
     */
    private final long workerIdBits = 10L;
    /**
     * 自增ID的最大值
     */
    protected final long maxSequence = ~(-1L << sequenceBits);
    /**
     * 机器标识ID向左移12位
     */
    protected final long workerIdShift = sequenceBits;
    /**
     * 时间截向左移22位(10+12)
     */
    protected final long timestampLeftShift = sequenceBits + workerIdBits;
    /**
     * 机器标识
     */
    protected long workerId;

    /**
     * 序列号
     */
    private long sequence = 0L;
    /**
     * 上一次时间戳
     */
    private long lastTimestamp = -1L;
    /**
     * 生成id的最大时间戳，当前时间戳超过 maxTimestamp，生成 com.goshine.id 将为负数
     * 计算公式：~(-1L << timestampBits) + epoch
     */
    protected long maxTimestamp;

    protected final IdWorkerRepository idWorkerRepository;

    public IdSnowflakeGenerator(IdWorkerRepository idWorkerRepository) {
        this.idWorkerRepository = idWorkerRepository;
    }

    @Override
    public String name() {
        return "snowflake";
    }

    @Override
    public Long getId(Integer appId) {
        long currentTimestamp = timeMillis();
        if (currentTimestamp > maxTimestamp) {
            throw new BizException("current timestamp is over maxTimeStamp, the generate id will be negative");
        }

        // 时钟回拨
        if (currentTimestamp < lastTimestamp) {
            long offset = lastTimestamp - currentTimestamp;
            if (offset <= 5) {
                try {
                    wait(offset << 1);
                    currentTimestamp = timeMillis();
                    if (currentTimestamp < lastTimestamp) {
                        LOGGER.error("clock back!!!");
                        throw new BizException("clock back");
                    }
                } catch (InterruptedException e) {
                    LOGGER.error("wait interrupted", e);
                    throw new BizException("clock back");
                }
            } else {
                throw new BizException("clock back");
            }
        }

        //如果是同一时间生成的，则进行毫秒内序列
        if (lastTimestamp == currentTimestamp) {
            // 相同毫秒内，序列号自增
            sequence = (sequence + 1) & maxSequence;
            if (sequence == 0) {
                // seq 为0的时候表示是同一毫秒的序列数已经达到最大，也就是下一毫秒时间对开始，因此对seq做随机
                sequence = RandomUtils.nextInt(1, 999);
                currentTimestamp = tilNextMillis();
            }
        } else {
            // 如果是新的ms开始，也对seq做随机
            sequence = RandomUtils.nextInt(1, 999);
        }

        //上次生成ID的时间戳
        lastTimestamp = currentTimestamp;

        //移位并通过或运算拼到一起组成64位的ID
        return ((currentTimestamp - epoch) << timestampLeftShift)
                | (workerId << workerIdShift)
                | sequence;

    }

    @Override
    public Set<Long> getIds(Integer appId, int step) {
        Set<Long> ids = new HashSet<>();
        for (int i = 0; i < step; i++) {
            try {
                Long id = getId(appId);
                if (null == id) {
                    continue;
                }
                ids.add(id);
            } catch (Exception e) {
                LOGGER.warn("Failed to get id, {}", e.getMessage());
                continue;
            }
        }
        return ids;
    }

    /**
     * 阻塞到下一个毫秒，直到获得新的时间戳
     *
     * @return 当前时间戳
     */
    protected long tilNextMillis() {
        long timestamp = timeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = timeMillis();
        }
        return timestamp;
    }

    private long timeMillis() {
        return System.currentTimeMillis();
    }
}
